package com.yb.flowable.service;

import com.alibaba.fastjson.JSONObject;
import com.yb.flowable.common.LoginUser;
import com.yb.flowable.common.LoginUserUtils;
import com.yb.flowable.param.Vacation;
import com.yb.flowable.param.VacationInfo;
import com.yb.flowable.param.VacationTask;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.common.engine.impl.util.IoUtil;
import org.flowable.engine.ProcessEngine;
import org.flowable.engine.ProcessEngineConfiguration;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.runtime.Execution;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.idm.api.Group;
import org.flowable.idm.api.Picture;
import org.flowable.idm.api.User;
import org.flowable.image.ProcessDiagramGenerator;
import org.flowable.task.api.Task;
import org.flowable.task.api.history.HistoricTaskInstance;
import org.flowable.variable.api.history.HistoricVariableInstance;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import sun.misc.BASE64Encoder;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Description:
 * 运行常用表act_ru_variable/act_ru_task/act_ru_execution/act_ru_identitylink/act_ru_actinst
 * 储存常用表act_re_procdef/act_re_deployment
 * 历史常用表act_hi_procinst/act_hi_taskinst/act_hi_varinst/act_hi_identitylink/act_hi_comment/act_hi_actinst
 *
 * @author biaoyang
 * date 2019/3/12 001217:59
 */
@Slf4j
@Service
@AllArgsConstructor
public class MyFlowableService {
    public static final String PROCESS_KEY = "qj";
    public static final String MONITOR = "monitor";
    public static final String HEAD_TEACHER = "headTeacher";
    public static final String TEACH_TEACHER = "teachTeacher";
    public static final String AUDITOR_GROUP = "auditor_group";
    private final ProcessEngine processEngine;

    /**
     * 请假流程审批人信息查询
     *
     * @return
     */
    public List<JSONObject> queryAuditorInfo() {
        //获取请假审批用户组的用户信息(User是接口,无法json反序列化成功)
        List<User> list = processEngine.getIdentityService().createUserQuery().memberOfGroup(AUDITOR_GROUP).list();
        if (CollectionUtils.isNotEmpty(list)) {
            return list.stream().map(s -> {
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("userId", s.getId());
                jsonObject.put("type", s.getFirstName());
                jsonObject.put("username", s.getDisplayName());
                //由于图片的字节数组输出时过大,这里就不弄头像图片了(时间过长)
                return jsonObject;
            }).collect(Collectors.toList());
        }
        return null;
    }

    /**
     * 为用户关联图片
     *
     * @param picturePath
     * @param userId
     */
    public void setPicture(String picturePath, String userId) {
        ClassPathResource resource = new ClassPathResource(picturePath);
        try {
            //获取输入流
            InputStream in = resource.getInputStream();
            //可能溢出,简单起见就不考虑太多,如果太大就要另外想办法,比如一次传入固定长度byte[]
            byte[] bytes = new byte[in.available()];
            //将图片内容写入字节数组
            in.read(bytes);
            //关闭流
            IoUtil.closeSilently(in);
            //获取文件名
            String filename = resource.getFilename();
            //为用户添加头像Picture第二个参数直接使用文件名(别只使用后缀了)
            processEngine.getIdentityService().setUserPicture(userId, new Picture(bytes, filename));
        } catch (IOException e) {
            log.info("头像获取失败:{}", e.getMessage());
        }
    }

    /**
     * 初始化审批者信息(一般都是写死的,如果需要更换,直接使用flowable更换用户这边的信息即可)
     *
     * @return
     */
    public String initAuditor() {
        //简单初始化用户和group信息(存储在flowable主要是头像信息和名称和用户id)
        User userA = processEngine.getIdentityService().newUser("1");
        userA.setDisplayName("jackson");
        //直接把审批用户的id按审批顺序拼接成字符串如下,
        //然后把这个用户添加到请假审批组,然后通过请假审批组来获取对应的审批用户id
        //(因为流程是已经定死了的,所以审批人数量是定死了的,你可以更换审批用户id来更换审批用户)
        userA.setFirstName(MONITOR);
        User userB = processEngine.getIdentityService().newUser("2");
        userB.setDisplayName("rose");
        userB.setFirstName(HEAD_TEACHER);
        User userC = processEngine.getIdentityService().newUser("3");
        userC.setDisplayName("tom");
        userC.setFirstName(TEACH_TEACHER);
        //保存用户信息
        processEngine.getIdentityService().saveUser(userA);
        processEngine.getIdentityService().saveUser(userB);
        processEngine.getIdentityService().saveUser(userC);
        //为用户关联图片
        setPicture("headImage\\1.jpeg", "1");
        setPicture("headImage\\2.jpeg", "2");
        setPicture("headImage\\3.jpeg", "3");
        //请假审批组(这个就是设置一个组关联所有的审批人,而不是设置不同的组)
        Group group = processEngine.getIdentityService().newGroup(AUDITOR_GROUP);
        group.setName("请假审批组");
        //保存组
        processEngine.getIdentityService().saveGroup(group);
        //请假审批组用户关联组(通过把第一个审批人放在组里,然后通过第一个审批人找第二个,依次类推)
        processEngine.getIdentityService().createMembership(userA.getId(), group.getId());
        processEngine.getIdentityService().createMembership(userB.getId(), group.getId());
        processEngine.getIdentityService().createMembership(userC.getId(), group.getId());
        //返回操作
        return "初始化数据成功";
    }

    /**
     * 填写请假信息开始请假
     *
     * @param vacation
     * @return
     */
    public String startVacation(VacationInfo vacation) {
        //获取登录用户ID信息
        String userId = LoginUserUtils.getUserId().orElseThrow(() -> new RuntimeException("无效的用户登录信息"));
        //判断用户是否还有未完成的请假流程,如果有则不允许其再申请请假流程
        List<ProcessInstance> instances = processEngine.getRuntimeService().createProcessInstanceQuery().startedBy(userId).list();
        if (CollectionUtils.isNotEmpty(instances)) {
            throw new RuntimeException("");
        }
        //获取各个审批组的用户(设定审批组只能有一个审批用户(第一个审批用户))
        List<User> list = Optional.ofNullable(processEngine.getIdentityService()
                .createUserQuery().memberOfGroup(AUDITOR_GROUP).list()).orElseThrow(() -> new RuntimeException("获取审批者信息异常"));
        //添加流程变量数据
        Map<String, Object> variables = new HashMap<>(2);
        //判断并处理数据(即便是有多个审批用户,由于map的key不重复性,也会取到一个用户的)
        list.forEach(s -> {
            //这里不是直接在流程图的xml里写死assignee的名称,而是通过传递变量来设置
            if (MONITOR.equals(s.getFirstName())) {
                variables.put(MONITOR, s.getId());
            }
            if (HEAD_TEACHER.equals(s.getFirstName())) {
                variables.put(HEAD_TEACHER, s.getId());
            }
            if (TEACH_TEACHER.equals(s.getFirstName())) {
                variables.put(TEACH_TEACHER, s.getId());
            }
        });
        //需要在流程开始前设置流程发起人(必要的)(实测必须在流程开始前设置才有)
        //当前已通过身份验证的用户的ID,如果没有用户通过身份验证,则该变量不可用,方法的参数作为流程启动人
        processEngine.getIdentityService().setAuthenticatedUserId(userId);
        //根据流程图(流程图的流程id)开始流程信息
        ProcessInstance instance = processEngine.getRuntimeService().startProcessInstanceByKey(PROCESS_KEY, variables);
        //查询当前任务--singleResult查询单个实例,返回多个则抛出FlowableException异常
        Task task = processEngine.getTaskService().createTaskQuery().processInstanceId(instance.getId()).singleResult();
        //声明任务属于谁(这里设定用户名是唯一的,这需要唯一值,用来查询相关联的任务信息,不唯一,将导致查询出别人的任务)
        processEngine.getTaskService().claim(task.getId(), userId);
        //设置任务信息
        Map<String, Object> map = new HashMap<>(4);
        map.put("applyUser", userId);
        map.put("days", vacation.getDays());
        map.put("reason", vacation.getReason());
        //完成任务
        processEngine.getTaskService().complete(task.getId(), map);
        //返回信息
        return "操作成功";
    }

    /**
     * 查询我的请假信息
     *
     * @return
     */
    public List<Vacation> queryMyVacation() {
        //获取登录用户信息
        LoginUser loginUser = LoginUserUtils.getUser().orElseThrow(() -> new RuntimeException("无效的用户登录信息"));
        //查询我发起的流程信息startedBy这里其实是用户id,这个需要根据设置发起人的时候使用的来,不过需要保证唯一性,建议使用userId
        List<ProcessInstance> list = processEngine.getRuntimeService().createProcessInstanceQuery().startedBy(loginUser.getUserId()).list();
        //判断并处理结果
        if (CollectionUtils.isNotEmpty(list)) {
            return list.stream().map(s -> {
                //根据流程实例id获取流程实例中的变量信息
                Double days = processEngine.getRuntimeService().getVariable(s.getId(), "days", Double.class);
                String reason = processEngine.getRuntimeService().getVariable(s.getId(), "reason", String.class);
                //封装数据
                Vacation vacation = new Vacation();
                vacation.setApplyTime(s.getStartTime());
                vacation.setApplyStatus(s.isEnded() ? "完成" : "审批中");
                //由于姓名是没有的,这里直接使用用户名
                vacation.setApplyUser(loginUser.getUsername());
                vacation.setDays(days);
                vacation.setReason(reason);
                //查询审批用户信息(这里由于不是直线的那种流程,所以这里直接返回对应的审批用户信息,由对应的前端进行逻辑判断处理展示)
                vacation.setAuditorInfos(queryAuditorInfo());
                return vacation;
            }).collect(Collectors.toList());
        }
        //返回数据
        return null;
    }

    /**
     * 查询我的请假记录(请假流程完成)
     *
     * @return
     */
    public List<Vacation> myVacationRecord() {
        //获取登录用户信息
        LoginUser loginUser = LoginUserUtils.getUser().orElseThrow(() -> new RuntimeException("无效的用户登录信息"));
        //初始化请假任务集合
        List<Vacation> vacations = new ArrayList<>(10);
        //因为流程走过之后是不会留存数据的,所以只能去历史流程实例里去获取数据
        List<HistoricProcessInstance> list = processEngine.getHistoryService().createHistoricProcessInstanceQuery()
                //involvedUser获取的是与用户相关的流程实例而startedBy获取的是与用户相关且是流程发起者的实例,而且这里指定流程定义key可避免查询到其他流程
                //finished()是查询已经完成的流程,使用startedBy是因为我的请假记录肯定是我发起的请假流程,我不是我参与的(可能包括我审批的)
                .processDefinitionKey(PROCESS_KEY).startedBy(loginUser.getUserId()).finished().orderByProcessInstanceEndTime().desc().list();
        //判断并处理数据
        if (CollectionUtils.isNotEmpty(list)) {
            list.forEach(s -> {
                Vacation vacation = new Vacation();
                vacation.setApplyUser(loginUser.getUsername());
                vacation.setApplyTime(s.getStartTime());
                //因为只有流程走完之后数据才会到历史里去,而且审批中的请假记录也是不完整的,所以请假记录里只有请假完成的信息
                vacation.setApplyStatus("完成");
                Map<String, Object> processVariables = s.getProcessVariables();
                //因为只能获取到变量对应的名称和值无法给实体对应字段赋值,所以调用工具方法通过变量名获取对应的字段,并为其赋值
                //封装实体
                vacations.add(vacation);
            });
        }
        //返回数据
        return vacations;
    }

    //&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&

    /**
     * 查询我得审批信息
     *
     * @return
     */
    public List<VacationTask> queryMyAuditInfo() {
        //获取登录用户信息
        LoginUser loginUser = LoginUserUtils.getUser().orElseThrow(() -> new RuntimeException("无效的用户登录信息"));
        //初始化请假任务集合
        List<VacationTask> vacationTasks = new ArrayList<>(10);
        //查询该用户的任务列表(统一通过用户id去关联,设定assignee使用的是userId)
        List<Task> list = processEngine.getTaskService().createTaskQuery().taskAssignee(loginUser.getUserId()).orderByTaskCreateTime().desc().list();
        //判断并处理数据
        if (CollectionUtils.isNotEmpty(list)) {
            list.forEach(s -> {
                VacationTask task = new VacationTask();
                task.setTaskId(s.getId());
                task.setTaskName(s.getName());
                task.setCreateDate(s.getCreateTime());
                task.setAuditor(loginUser.getUsername());
                //通过流程实例id获取流程实例
                ProcessInstance instance = processEngine.getRuntimeService()
                        .createProcessInstanceQuery()
                        .processInstanceId(s.getProcessInstanceId())
                        .singleResult();
                //根据流程实例id获取流程实例中的变量信息(实测使用流程id和任务的执行id都可以查询到,api是用的是执行id)
                Double days = processEngine.getRuntimeService().getVariable(s.getExecutionId(), "days", Double.class);
                String reason = processEngine.getRuntimeService().getVariable(instance.getId(), "reason", String.class);
                //封装数据
                Vacation vacation = new Vacation();
                vacation.setApplyTime(instance.getStartTime());
                //通过流程发起人id获取其用户名
                User user = processEngine.getIdentityService().createUserQuery().userId(instance.getStartUserId()).singleResult();
                //由于姓名是没有的,这里直接使用用户名
                vacation.setApplyUser(Objects.isNull(user) ? null : user.getDisplayName());
                vacation.setDays(days);
                vacation.setReason(reason);
                //封装请假信息到请假任务里
                task.setVacation(vacation);
                //添加到集合
                vacationTasks.add(task);
            });
        }
        //------------------------------------------------------------------------------------------------
        //------------------------------------------------------------------------------------------------
        //返回信息
        return vacationTasks;
    }

    /**
     * 查询我的审批记录
     *
     * @return
     */
    public List<Vacation> queryMyAuditRecord() {
        //获取登录用户信息
        LoginUser loginUser = LoginUserUtils.getUser().orElseThrow(() -> new RuntimeException("无效的用户登录信息"));
        //初始化请假任务集合
        List<Vacation> vacations = new ArrayList<>(10);
        //因为流程走过之后是不会留存数据的,所以只能去历史流程实例里去获取数据
        List<HistoricProcessInstance> list = processEngine.getHistoryService().createHistoricProcessInstanceQuery()
                .processDefinitionKey(PROCESS_KEY).involvedUser(loginUser.getUserId()).orderByProcessInstanceEndTime().desc().list();
        //初始化审批任务人名集合
        List<String> auditTaskNames = new ArrayList<>(3);
        auditTaskNames.add("班长审批");
        auditTaskNames.add("班主任审批");
        auditTaskNames.add("任课老师审批");
        //判断并处理集合数据
        if (CollectionUtils.isNotEmpty(list)) {
            list.forEach(s -> {
                List<HistoricTaskInstance> hisTaskList = processEngine.getHistoryService().createHistoricTaskInstanceQuery()
                        .processInstanceId(s.getId())
                        .processFinished()
                        //流程任务处理人
                        .taskAssignee(loginUser.getUserId())
                        //流程任务名
                        .taskNameIn(auditTaskNames)
                        .orderByHistoricTaskInstanceEndTime()
                        .desc().list();
                //获取历史任务实例里的处理人查看是不是我的
                if (CollectionUtils.isNotEmpty(hisTaskList)) {
                    hisTaskList.forEach(f -> {
                        //判断处理任务的人是不是我
                        if (loginUser.getUserId().equals(f.getAssignee())) {
                            Vacation vacation = new Vacation();
                            vacation.setApplyStatus("完成");
                            vacation.setApplyUser(s.getStartUserId());
                            vacation.setApplyTime(s.getStartTime());
                            //获取流程实例的变量
                            List<HistoricVariableInstance> hisVars = processEngine.getHistoryService()
                                    .createHistoricVariableInstanceQuery().processInstanceId(s.getId()).list();
                            //封装请假信息列表
                            vacations.add(vacation);
                        }
                    });
                }
            });
        }
        //返回数据
        return vacations;
    }

    /**
     * @return
     */
    public String auditTask(Boolean isAudit) {
        //获取登录用户信息
        LoginUser loginUser = LoginUserUtils.getUser().orElseThrow(() -> new RuntimeException("无效的用户登录信息"));
        //查询是否有自己的审批任务(指定流程后,任务就只会有一个了,不可能一个人审批几次)
        Task tasks = processEngine.getTaskService().createTaskQuery().processDefinitionKey(PROCESS_KEY)
                //这么做是有问题的,即便定义的是同一个流程,也可能会有多个不同的人发起的流程实例,这里也会是多个任务,
                //哪里有领导每次都只能看得到一个人的请假的情况
                .taskCandidateOrAssigned(loginUser.getUserId()).singleResult();
        Map<String, Object> vars = new HashMap<>(4);
        vars.put("auditorId", loginUser.getUserId());
        vars.put("auditorName", loginUser.getUsername());
        vars.put("auditResult", isAudit ? "批准" : "不批准");
        vars.put("auditDate", LocalDateTime.now());
        //声明审批人
        processEngine.getTaskService().claim(tasks.getId(), loginUser.getUserId());
        //完成任务
        processEngine.getTaskService().complete(tasks.getId(), vars);
        //返回结果
        return "操作成功";
    }

    /**
     * 下载(查看)流程图(PNG),未被标红的是没有开始的流程
     *
     * @param response
     * @param processInstanceId 流程实例id
     */
    public void generateProcessDiagram(HttpServletResponse response, String processInstanceId) {
        //初始化一个封装id的集合
        List<String> activityIds = new ArrayList<>();
        //根据流程实例id获取流程实例-->可以不查看(下载)已经完成的流程通过流程,
        //想看已经完成的流程图的话可以通过History查询去查询操作接口
        ProcessInstance instance = processEngine.getRuntimeService().createProcessInstanceQuery()
                .processInstanceId(processInstanceId).singleResult();
        if (instance == null) {
            throw new RuntimeException("查询流程不存在或者流程已经走完了");
        }
        //其实可以直接通过流程实例获取流程的定义id的,不用通过任务去获取
        String processDefinitionId = instance.getProcessDefinitionId();
        //通过流程定义id获取正在执行的对象列表(显示标红所需)
        List<Execution> list = processEngine.getRuntimeService().createExecutionQuery()
                .processDefinitionId(processDefinitionId).list();
        //判断并处理程序
        if (CollectionUtils.isNotEmpty(list)) {
            list.forEach(execution -> {
                List<String> ids = processEngine.getRuntimeService().getActiveActivityIds(execution.getId());
                //把正在执行的id添加到集合里
                activityIds.addAll(ids);
            });
        }
        //获取流程图模型
        BpmnModel bpmnModel = processEngine.getRepositoryService().getBpmnModel(instance.getProcessDefinitionId());
        //获取流程图表生成对象
        ProcessEngineConfiguration processConf = processEngine.getProcessEngineConfiguration();
        ProcessDiagramGenerator generator = processConf.getProcessDiagramGenerator();
        //实测这个流能生成图,正在活动的任务被标红,并且人物名没有乱码(只有两个参数的方法,会生成乱码名称和不标红当前任务)
        InputStream inputStream = generator.generateDiagram(bpmnModel, "png", activityIds, new ArrayList<>(),
                processConf.getActivityFontName(), processConf.getLabelFontName(), processConf.getAnnotationFontName(),
                processConf.getClassLoader(), 1.0, false);
        //声明变量
        OutputStream outputStream = null;
        byte[] bytes = new byte[1024];
        int length;
        try {
            //获取响应流
            outputStream = response.getOutputStream();
            //输出流文件
            while ((length = inputStream.read(bytes)) != -1) {
                outputStream.write(bytes, 0, length);
            }
        } catch (IOException e) {
            log.info("输出流程图进展时异常:{}", e.getMessage());
        } finally {
            //后用先关
            IoUtil.closeSilently(outputStream);
            //先用后关
            IoUtil.closeSilently(inputStream);
        }
    }

}
